<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>工程项目知识图谱</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            overflow: hidden;
            background-color: #f5f5f5;
        }
        
        #graph-container {
            width: 100%;
            height: 90vh;
            border: 1px solid #ddd;
            background-color: white;
            border-radius: 5px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        
        .node {
            cursor: pointer;
        }
        
        .node circle {
            stroke: #fff;
            stroke-width: 2px;
        }
        
        .node text {
            font-size: 12px;
        }
        
        .link {
            fill: none;
            stroke: #ccc;
            stroke-width: 1.5px;
        }
        
        .context-menu {
            position: absolute;
            display: none;
            background-color: #fff;
            border: 1px solid #ddd;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 1000;
        }
        
        .context-menu-item {
            padding: 8px 15px;
            cursor: pointer;
        }
        
        .context-menu-item:hover {
            background-color: #f0f0f0;
        }
        
        .tooltip {
            position: absolute;
            padding: 8px;
            background: rgba(0, 0, 0, 0.8);
            color: #fff;
            border-radius: 4px;
            font-size: 12px;
            pointer-events: none;
            z-index: 10;
        }
    </style>
</head>
<body>
    <h1>工程项目知识图谱</h1>
    <div id="graph-container"></div>
    <div id="context-menu" class="context-menu"></div>
    <div class="tooltip" id="tooltip"></div>

    <script>
        // 模拟数据服务
        const mockDataService = {
            getData: function(url) {
                // 模拟API响应延迟
                return new Promise((resolve) => {
                    setTimeout(() => {
                        // 根据不同的URL返回不同的模拟数据
                        if (url.includes('/1')) {
                            resolve({
                                content: [
                                    { name: "阳光新城", kev2: "SG-2023-001" },
                                    { name: "未来科技园", kev2: "SG-2023-002" }
                                ]
                            });
                        } else if (url.includes('key1=阳光新城') && !url.includes('key2') && !url.includes('code')) {
                            resolve({
                                content: [
                                    { name: "1号楼", jzbm: "BLD-001", xx: "A1" },
                                    { name: "2号楼", jzbm: "BLD-002", xx: "A2" }
                                ]
                            });
                        } else if (url.includes('code=A1')) {
                            resolve({
                                content: { name: "建设工程规划许可证-2023-001" }
                            });
                        } else if (url.includes('key1=阳光新城') && url.includes('key2=BLD-001')) {
                            resolve({
                                content: [
                                    { name: "F1" },
                                    { name: "F2" }
                                ]
                            });
                        } else if (url.includes('key1=阳光新城') && !url.includes('key2')) {
                            resolve({
                                content: [
                                    { name: "单元A" },
                                    { name: "单元B" }
                                ]
                            });
                        } else {
                            resolve({
                                content: [
                                    { name: "F1-101" },
                                    { name: "F1-102" }
                                ]
                            });
                        }
                    }, 300);
                });
            }
        };

        // 知识图谱配置
        const config = [
            {
                "nodeId": "node1",
                "nodeName": "项目"
            },
            {
                "nodeId": "node2",
                "parentNode": "node1",
                "relationName": "项目",
                "nodesFrom": "http://sxxxx/1",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "return result.content",
                "nodeName": "return name",
                "fieldAliasNames": [
                    "return result.content.kev2:cnName1"
                ]
            },
            {
                "nodeId": "node3",
                "parentNode": "node2",
                "relationName": "建筑",
                "nodesFrom": "http://sxxxx/?key1={node2.name}",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "return result.content",
                "nodeName": "return name"
            },
            {
                "nodeId": "node8",
                "parentNode": "node2",
                "relationName": "规划许可",
                "nodesFrom": "http://sxxxx/?key1={node2.name}&&code={node3.xx}",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "return result.content",
                "nodeName": "return result.content.name"
            },
            {
                "nodeId": "node4",
                "parentNode": "node2",
                "relationName": "审批结论",
                "nodesFrom": "http://sxxxx/?key1={node2.name}&&code={node8.xx}",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "",
                "nodeName": "return result.content.name"
            },
            {
                "nodeId": "node5",
                "parentNode": "node3",
                "relationName": "单元",
                "nodesFrom": "http://sxxxx/?key1={node2.name}",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "return result.content",
                "nodeName": "return name",
            },
            {
                "nodeId": "node6",
                "parentNode": "node5",
                "relationName": "楼层",
                "nodesFrom": "http://sxxxx/?key1={node2.name}&&key2={node3.jzbm}",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "return result.content",
                "nodeName": "return name",
            },
            {
                "nodeId": "node7",
                "parentNode": "node6",
                "parentClickMenu": "加载房间",
                "relationName": "房间",
                "nodesFrom": "http://sxxxx/?key1={node2.name}&&key2={node3.jzbm}",
                "token": "AgCIM DataStore/NONE",
                "nodeArray": "return result.content",
                "nodeName": "return name",
            }
        ];

        // 主应用程序
        class KnowledgeGraph {
            constructor(containerId, config) {
                this.container = d3.select(`#${containerId}`);
                this.config = config;
                this.graphData = { nodes: [], links: [] };
                this.nodeMap = new Map();
                this.contextMenu = d3.select('#context-menu');
                this.tooltip = d3.select('#tooltip');
                
                // 初始化SVG
                this.svg = this.container.append('svg')
                    .attr('width', '100%')
                    .attr('height', '100%');
                
                this.zoom = d3.zoom()
                    .scaleExtent([0.1, 5])
                    .on('zoom', (event) => {
                        this.g.attr('transform', event.transform);
                    });
                
                this.svg.call(this.zoom);
                
                this.g = this.svg.append('g');
                
                // 初始化力导向图
                this.simulation = d3.forceSimulation()
                    .force('link', d3.forceLink().id(d => d.id).distance(100))
                    .force('charge', d3.forceManyBody().strength(-500))
                    .force('center', d3.forceCenter(
                        this.container.node().clientWidth / 2, 
                        this.container.node().clientHeight / 2
                    ));
                
                // 添加根节点
                this.addRootNode();
                
                // 绑定右键菜单事件
                this.bindContextMenu();
            }
            
            addRootNode() {
                const rootConfig = this.config.find(item => item.nodeId === 'node1');
                const rootNode = {
                    id: 'node1',
                    name: rootConfig.nodeName,
                    type: 'root',
                    expanded: false,
                    level: 0,
                    config: rootConfig
                };
                
                this.graphData.nodes.push(rootNode);
                this.nodeMap.set('node1', rootNode);
                this.updateGraph();
                
                // 自动展开根节点
                this.expandNode(rootNode);
            }
            
            async expandNode(node) {
                if (node.expanded) return;
                
                node.expanded = true;
                
                // 查找配置中该节点的子节点配置
                const childConfigs = this.config.filter(item => item.parentNode === node.id);
                
                for (const config of childConfigs) {
                    // 如果是需要点击菜单才加载的节点，跳过自动加载
                    if (config.parentClickMenu) continue;
                    
                    try {
                        // 构建请求URL
                        let url = config.nodesFrom;
                        
                        // 替换URL中的变量
                        const matches = url.match(/\{node\d+\.[a-zA-Z]+\}/g) || [];
                        for (const match of matches) {
                            const [nodeId, field] = match.slice(1, -1).split('.');
                            const parentNode = this.nodeMap.get(nodeId);
                            if (parentNode && parentNode.data && parentNode.data[field]) {
                                url = url.replace(match, parentNode.data[field]);
                            }
                        }
                        
                        // 获取数据
                        const result = await mockDataService.getData(url);
                        
                        // 解析节点数组
                        let nodeArray = [];
                        if (config.nodeArray) {
                            // 简单实现，实际应该解析表达式
                            nodeArray = result.content;
                        } else {
                            nodeArray = [result.content];
                        }
                        
                        // 添加子节点
                        for (const item of nodeArray) {
                            const nodeName = config.nodeName.startsWith('return ') 
                                ? item[config.nodeName.replace('return ', '')] 
                                : config.nodeName;
                                
                            const childNode = {
                                id: `${config.nodeId}-${nodeArray.indexOf(item) + 1}`,
                                name: nodeName,
                                type: config.nodeId,
                                expanded: false,
                                level: node.level + 1,
                                config: config,
                                data: item,
                                parentId: node.id
                            };
                            
                            // 处理别名
                            if (config.fieldAliasNames) {
                                config.fieldAliasNames.forEach(alias => {
                                    const [path, aliasName] = alias.split(':');
                                    if (path.startsWith('return ')) {
                                        const fieldPath = path.replace('return ', '').split('.');
                                        let value = item;
                                        for (const field of fieldPath) {
                                            if (value && value[field]) {
                                                value = value[field];
                                            } else {
                                                value = null;
                                                break;
                                            }
                                        }
                                        if (value) {
                                            childNode[aliasName] = value;
                                        }
                                    }
                                });
                            }
                            
                            this.graphData.nodes.push(childNode);
                            this.nodeMap.set(childNode.id, childNode);
                            
                            // 添加关系
                            this.graphData.links.push({
                                source: node.id,
                                target: childNode.id,
                                relation: config.relationName
                            });
                        }
                        
                    } catch (error) {
                        console.error(`Error expanding node ${node.id}:`, error);
                    }
                }
                
                this.updateGraph();
            }
            
            updateGraph() {
                // 更新节点
                const nodes = this.g.selectAll('.node')
                    .data(this.graphData.nodes, d => d.id);
                
                // 进入节点
                const nodeEnter = nodes.enter().append('g')
                    .attr('class', 'node')
                    .attr('id', d => `node-${d.id}`)
                    .attr('transform', d => `translate(${d.x || 0},${d.y || 0})`)
                    .on('click', (event, d) => {
                        this.expandNode(d);
                    })
                    .on('contextmenu', (event, d) => {
                        event.preventDefault();
                        this.showContextMenu(event, d);
                    })
                    .on('mouseover', (event, d) => {
                        this.showTooltip(event, d);
                    })
                    .on('mouseout', () => {
                        this.tooltip.style('display', 'none');
                    });
                
                // 添加圆形
                nodeEnter.append('circle')
                    .attr('r', d => this.getNodeRadius(d))
                    .attr('fill', d => this.getNodeColor(d));
                
                // 添加文本
                nodeEnter.append('text')
                    .attr('dy', 4)
                    .attr('text-anchor', 'middle')
                    .text(d => d.name)
                    .style('font-size', '12px');
                
                // 更新节点位置
                nodes.merge(nodeEnter)
                    .transition()
                    .duration(500)
                    .attr('transform', d => `translate(${d.x},${d.y})`);
                
                // 退出节点
                nodes.exit().remove();
                
                // 更新连线
                const links = this.g.selectAll('.link')
                    .data(this.graphData.links, d => `${d.source.id || d.source}-${d.target.id || d.target}`);
                
                links.enter().append('path')
                    .attr('class', 'link')
                    .attr('marker-end', 'url(#arrowhead)')
                    .merge(links);
                
                links.exit().remove();
                
                // 添加箭头标记
                if (this.g.select('defs').empty()) {
                    const defs = this.g.append('defs');
                    
                    defs.append('marker')
                        .attr('id', 'arrowhead')
                        .attr('viewBox', '0 -5 10 10')
                        .attr('refX', 18)
                        .attr('refY', 0)
                        .attr('orient', 'auto')
                        .attr('markerWidth', 6)
                        .attr('markerHeight', 6)
                        .attr('xoverflow', 'visible')
                        .append('path')
                        .attr('d', 'M0,-5L10,0L0,5')
                        .attr('fill', '#999');
                }
                
                // 添加关系标签
                const linkText = this.g.selectAll('.link-text')
                    .data(this.graphData.links, d => `${d.source.id || d.source}-${d.target.id || d.target}`);
                
                linkText.enter().append('text')
                    .attr('class', 'link-text')
                    .attr('font-size', '10px')
                    .attr('fill', '#666')
                    .text(d => d.relation);
                
                linkText.exit().remove();
                
                // 更新力导向图
                this.simulation
                    .nodes(this.graphData.nodes)
                    .force('link', d3.forceLink(this.graphData.links).id(d => d.id).distance(100))
                    .on('tick', () => {
                        // 更新连线
                        this.g.selectAll('.link')
                            .attr('d', d => {
                                const source = typeof d.source === 'string' 
                                    ? this.nodeMap.get(d.source) 
                                    : d.source;
                                const target = typeof d.target === 'string' 
                                    ? this.nodeMap.get(d.target) 
                                    : d.target;
                                
                                if (!source || !target) return '';
                                
                                const dx = target.x - source.x;
                                const dy = target.y - source.y;
                                const dr = Math.sqrt(dx * dx + dy * dy);
                                
                                return `M${source.x},${source.y}A${dr},${dr} 0 0,1 ${target.x},${target.y}`;
                            });
                        
                        // 更新关系标签位置
                        this.g.selectAll('.link-text')
                            .attr('x', d => {
                                const source = typeof d.source === 'string' 
                                    ? this.nodeMap.get(d.source) 
                                    : d.source;
                                const target = typeof d.target === 'string' 
                                    ? this.nodeMap.get(d.target) 
                                    : d.target;
                                
                                return (source.x + target.x) / 2;
                            })
                            .attr('y', d => {
                                const source = typeof d.source === 'string' 
                                    ? this.nodeMap.get(d.source) 
                                    : d.source;
                                const target = typeof d.target === 'string' 
                                    ? this.nodeMap.get(d.target) 
                                    : d.target;
                                
                                return (source.y + target.y) / 2;
                            });
                    });
                
                this.simulation.alpha(1).restart();
            }
            
            getNodeColor(node) {
                const colors = {
                    'node1': '#4CAF50',   // 根节点 - 绿色
                    'node2': '#2196F3',   // 项目 - 蓝色
                    'node3': '#FF9800',   // 建筑 - 橙色
                    'node8': '#9C27B0',   // 规划许可 - 紫色
                    'node4': '#F44336',   // 审批结论 - 红色
                    'node5': '#00BCD4',   // 单元 - 青色
                    'node6': '#607D8B',   // 楼层 - 灰色
                    'node7': '#8BC34A'    // 房间 - 浅绿色
                };
                return colors[node.type] || '#9E9E9E';
            }
            
            getNodeRadius(node) {
                const sizes = {
                    'node1': 20,   // 根节点
                    'node2': 15,   // 项目
                    'node3': 12,   // 建筑
                    'node8': 12,   // 规划许可
                    'node4': 10,   // 审批结论
                    'node5': 10,   // 单元
                    'node6': 8,    // 楼层
                    'node7': 6     // 房间
                };
                return sizes[node.type] || 8;
            }
            
            bindContextMenu() {
                // 隐藏右键菜单当点击其他地方时
                d3.select('body').on('click', () => {
                    this.contextMenu.style('display', 'none');
                });
            }
            
            showContextMenu(event, node) {
                event.preventDefault();
                
                // 查找该节点是否有需要右键菜单加载的子节点
                const menuItems = this.config
                    .filter(item => item.parentNode === node.id && item.parentClickMenu)
                    .map(item => ({
                        text: item.parentClickMenu,
                        action: () => this.expandNode(node)
                    }));
                
                if (menuItems.length === 0) return;
                
                // 显示右键菜单
                this.contextMenu
                    .style('left', `${event.pageX}px`)
                    .style('top', `${event.pageY}px`)
                    .style('display', 'block')
                    .html('');
                
                menuItems.forEach(item => {
                    this.contextMenu.append('div')
                        .attr('class', 'context-menu-item')
                        .text(item.text)
                        .on('click', () => {
                            item.action();
                            this.contextMenu.style('display', 'none');
                        });
                });
            }
            
            showTooltip(event, node) {
                let tooltipContent = `<strong>${node.name}</strong><br/>类型: ${node.type}`;
                
                // 添加额外属性
                if (node.data) {
                    tooltipContent += '<br/><br/>属性:';
                    for (const key in node.data) {
                        tooltipContent += `<br/>${key}: ${node.data[key]}`;
                    }
                }
                
                // 添加别名
                if (node.cnName1) {
                    tooltipContent += `<br/>项目编号: ${node.cnName1}`;
                }
                
                this.tooltip
                    .style('left', `${event.pageX + 10}px`)
                    .style('top', `${event.pageY + 10}px`)
                    .style('display', 'block')
                    .html(tooltipContent);
            }
        }

        // 初始化知识图谱
        document.addEventListener('DOMContentLoaded', () => {
            const kg = new KnowledgeGraph('graph-container', config);
        });
    </script>
</body>
</html>